//! Logic for doing entity physics.

use craftdig_core::registrar::GameRegistrar;
use craftdig_datamodel::{
    entity::EntityState,
    world::{environment::WorldEnv, WorldView},
};
use craftdig_math::{Direction, RawCollision, Vec3f, AABB};

use crate::collision::{check_entity_terrain_collision, compute_collision_summary};

// TODO move this to the entity state probably
pub const JUMP_SPEED: f32 = 0.42;

/// Threshold for velocity below which we just round it to 0.
pub const VEL_THRESH: f32 = 0.003;

/// After we fall past this point we correct the y coordinate and velocity.
pub const VOID_THRESH: f32 = -50.0;

/// Used to prepare updates to the physical state of an entity.
pub trait PhysicsEntity {
    /// Used for collisions.  This is in the abstract.
    fn collider(&self) -> &AABB;

    /// Position of the origin of the entity.
    fn position(&self) -> Vec3f;

    /// Velocity of the entity.
    fn velocity(&self) -> Vec3f;

    /// If the entity should obey gravity.
    fn obeys_gravity(&self) -> bool;

    /// If the entity was on the ground as of the last tick.
    fn is_grounded(&self) -> bool;
}

/// Shim type wrapping an entity.
// TODO clean up this when we have a more sophisticated component design
pub struct EntityPhysState<'e> {
    ent: &'e EntityState,
    vel: Vec3f,
    grav: bool,
    grounded: bool,
}

impl<'e> EntityPhysState<'e> {
    /// Tries to construct an instance from an entity's state, if a phys
    /// component is set.
    pub fn try_from_entity_state(e: &'e EntityState) -> Option<Self> {
        let Some(phys) = e.phys() else {
            return None;
        };

        Some(Self {
            ent: e,
            vel: phys.velocity(),
            grav: phys.obey_gravity(),
            grounded: phys.is_grounded(),
        })
    }
}

impl<'e> PhysicsEntity for EntityPhysState<'e> {
    fn collider(&self) -> &AABB {
        self.ent.collider()
    }

    fn position(&self) -> Vec3f {
        self.ent.position()
    }

    fn velocity(&self) -> Vec3f {
        self.vel
    }

    fn obeys_gravity(&self) -> bool {
        self.grav
    }

    fn is_grounded(&self) -> bool {
        self.grounded
    }
}

/// Describes how an entity intends to move, which informs our physics update calculation.
///
/// This can be derived from player inputs, but also AI.
#[derive(Clone, Debug, Default)]
pub struct MoveIntent {
    /// Direction of movement/travel.
    movement: Vec3f,

    /// If jumping.
    jump: bool,
}

impl MoveIntent {
    pub fn new(movement: Vec3f, jump: bool) -> Self {
        Self { movement, jump }
    }

    pub fn new_move(movement: Vec3f) -> Self {
        Self::new(movement, false)
    }

    pub fn movement(&self) -> Vec3f {
        self.movement
    }

    pub fn jump(&self) -> bool {
        self.jump
    }
}

#[derive(Clone, Debug, Default)]
pub struct PhysicsUpdate {
    new_pos: Option<Vec3f>,
    new_vel: Option<Vec3f>,
    new_grounded: Option<bool>,
}

impl PhysicsUpdate {
    pub fn new(new_pos: Option<Vec3f>, new_vel: Option<Vec3f>, set_grounded: Option<bool>) -> Self {
        Self {
            new_pos,
            new_vel,
            new_grounded: set_grounded,
        }
    }

    pub fn new_pos(&self) -> Option<Vec3f> {
        self.new_pos
    }

    fn set_new_pos(&mut self, new_pos: Vec3f) {
        self.new_pos = Some(new_pos);
    }

    pub fn new_vel(&self) -> Option<Vec3f> {
        self.new_vel
    }

    pub fn set_new_vel(&mut self, new_vel: Vec3f) {
        self.new_vel = Some(new_vel);
    }

    pub fn set_new_grounded(&mut self, new_grounded: bool) {
        self.new_grounded = Some(new_grounded);
    }

    pub fn new_grounded(&self) -> Option<bool> {
        self.new_grounded
    }

    /// Applies the physics update to some entity's state.
    pub fn apply_to_entity(&mut self, entity: &mut EntityState) {
        if let Some(new_pos) = self.new_pos {
            entity.set_position(new_pos);
        }

        if let Some(new_vel) = self.new_vel {
            if let Some(phys) = entity.phys_mut() {
                phys.set_velocity(new_vel);
            }
        }

        if let Some(new_grounded) = self.new_grounded {
            if let Some(phys) = entity.phys_mut() {
                phys.set_grounded(new_grounded);
            }
        }
    }
}

pub fn compute_entity_physics_update(
    ent: &impl PhysicsEntity,
    mvmt: Option<&MoveIntent>,
    wv: &impl WorldView,
    reg: &GameRegistrar,
    env: &WorldEnv,
) -> PhysicsUpdate {
    let mut update = PhysicsUpdate::default();
    let grav = Vec3f::new(0.0, -0.42, 0.0);

    // FIXME this math is all screwed up
    let mcdt = 1.0 / 20.0;
    let tick_dt = env.phys_delta_time();
    let real_dt = env.phys_delta_time() / mcdt;
    let phys_norm = mcdt / env.phys_delta_time();
    eprintln!("real_dt={real_dt}");

    // Figure out the surface we're on and extract the slipperyness of it.
    // TODO fetch block we're standing on
    let real_slip = if ent.is_grounded() { 0.6 } else { 1.0 };

    // NEW MATH

    // Split the velocity into parts since the math works differently.
    let horiz_vel = ent.velocity().scale(Vec3f::new(1.0, 0.0, 1.0));
    let vert_vel = ent.velocity().scale(Vec3f::new(0.0, 1.0, 0.0));

    // Horizontal momentum adjustment, accounting for drag and whatnot.
    let h_momentum_comp = horiz_vel * real_slip * 0.91;

    // Horizontal accel adjustment for intended motion.
    let h_acc_comp = if let Some(mvmt) = mvmt {
        let speed = if ent.is_grounded() {
            // Ground speed.
            let effect_mult = 1.0; // TODO effects
            let move_mult = 1.0;
            0.1 * move_mult * effect_mult * f32::powf(0.6 / real_slip, 3.0)
        } else {
            // Air speed.
            let move_mult = 1.0;
            0.02 * move_mult
        };

        mvmt.movement() * speed
    } else {
        Vec3f::zero()
    };

    // Vertical momentum adjustment, accounting for drag.
    //
    // This won't make sense if we're jumping due to the deltatime adjustment,
    // we handle that separately.
    let v_vel = if ent.obeys_gravity() && !ent.is_grounded() {
        // Plus the gravity vector since its y component is negative.
        (vert_vel + grav * real_dt) * 0.98
    } else {
        vert_vel * 0.98
    };

    // Now adjust for the deltatime.
    let adj_h_vel = h_momentum_comp + h_acc_comp;

    // Final new velocity we adjust position with.
    let mut new_vel = adj_h_vel + vert_vel;

    // Fix NaNs, if they somehow got here.
    if new_vel.is_any_nan() {
        eprintln!("craftdig: [WARN] fixing NaN velocity");
        new_vel = Vec3f::zero();
    }

    // Apply velocity dropout.
    // TODO make this do it component-by-component
    if new_vel.magnitude() < VEL_THRESH {
        new_vel = Vec3f::zero();
    }

    // Also compute change in vel for jumping.
    let wants_jump = mvmt.map(|m| m.jump()).unwrap_or_default();
    let jump_dv = if wants_jump && ent.is_grounded() {
        update.set_new_grounded(false);
        Vec3f::new_y(JUMP_SPEED)
    } else {
        Vec3f::zero()
    };

    // Apply the new position, accounting for dt.  This is without accounting for collision nudges.
    let final_vel = adj_h_vel + v_vel;
    eprintln!("final_vel={final_vel:?}");
    let mut new_pos = ent.position() + final_vel + jump_dv;

    // Now do collisions!
    let eaabb = ent.collider();
    let block_reg = reg.block_ty_reg();
    let tcols = check_entity_terrain_collision(new_pos, eaabb, wv, block_reg);

    // Figure out what kinds of collisions we got.
    let col_summary = compute_collision_summary(tcols.iter().map(|tc| tc.col()));
    let nudge_vec = col_summary.nudge();

    #[cfg(test)]
    if !tcols.is_empty() {
        eprintln!("collisions ({:?}): {tcols:?}", col_summary.mask());
    }

    // Apply the correction.
    if !nudge_vec.is_any_nan() {
        new_pos += nudge_vec;
    } else {
        eprintln!("craftdig: [WARN] phys had NaN in collision nudge vector");
    }

    if !nudge_vec.is_roughly_zero() {
        eprintln!("craftdig: nudge is {nudge_vec:?}");
    }

    // If we landed on the ground, set the grounded flag.
    if col_summary.mask().has_dir(Direction::Down) {
        update.set_new_grounded(true);
    }

    // Also negate any velocity in a direction that we collided with something.
    if !nudge_vec.is_exactly_zero() {
        new_vel = new_vel.scale(col_summary.mask().compute_negating_scale());
    }

    // Set the new velocity to update the entity with.
    update.set_new_vel(final_vel);

    // And now a final correction in case we fall off the world.
    if new_pos.y() < VOID_THRESH {
        eprintln!("craftdig: fell off world, fixing position");
        new_pos = Vec3f::new(new_pos.x(), -VOID_THRESH, new_pos.z());
        update.set_new_vel(Vec3f::zero());
    }

    if !new_pos.is_any_nan() {
        update.set_new_pos(new_pos);
    } else {
        panic!("craftdig: position became NaN");
    }

    update
}

fn apply_vel_dropout(v: Vec3f) -> Vec3f {
    Vec3f::new(
        apply_vel_comp_dropout(v.x()),
        apply_vel_comp_dropout(v.y()),
        apply_vel_comp_dropout(v.z()),
    )
}

fn apply_vel_comp_dropout(v: f32) -> f32 {
    if v < VEL_THRESH {
        0.0
    } else {
        v
    }
}
